The IRIX semaphores and locks are especially tuned to efficiency in a multiprocessor environment.
In the traditional fork() call, the new process executes the identical program text as the old one; that is, both processes "return" from fork() and you distinguish them by the return code, which is 0 in the child process and the new process ID in the parent.
The sproc() call differs in that it takes as an argument the address of the function that should be executed by the new process. Often, each child process has a particular role to play, and the function that represents that work.
Another design is possible. The sproc() function has considerable overhead. It is inefficient to continually create and destroy child processes. In some applications, you may have to manage a flow of many, relatively short, activities which should be done in parallel. You do not want to create a new child process for each activity and destroy it afterward. Instead, you can create a pool containing a small number of general-purpose processes. When a piece of work needs to be done, you can dispatch one process to do it. The fragmentary code in Example 3-1 shows the general approach.
Example 3-1 : Partial Code to Manage a Pool of Processes
typedef void (*func)(void *arg) workFunc; struct oneSproc { struct oneSproc *next; /* -> next oneSproc ready to run */ workFunc calledFunc; /* -> function sproc is to call */ void *callArg; /* argument to pass to called func */ usema_t *sprocDone; /* optional sema to post on completion */ usema_t *sprocWait; /* sproc waits for work here */ } sprocList[NUMSPROCS]; usema_t *readySprocs; /* count represents sprocs ready to work */ uslock_t sprocListLock; /* mutex control of sprocList head */ struct oneSproc *sprocList; /* -> first ready oneSproc */ /* || Put a oneSproc structure on the ready list and sleep on it. || Called by a child process when its work is done. */ void sprocSleep(struct oneSproc *theSproc) { ussetlock(sprocListLock); /* acquire exclusive rights to sprocList */ theSproc->next = sprocList; /* put self on the list */ sprocList = theSproc; usunsetlock(sprocListLock); /* release sprocList */ usvsema(readySprocs); /* notify master, at least 1 on the list */ uspsema(theSproc->sprocWait);/* sleep until master posts me */ } /* || Body of a general-purpose child process. The argument, which must || be declared void* to match the sproc() prototype, is the oneSproc || structure that represents this process. The contents of that || struct, in particular sprocWait, are initialized by the parent. */ void childBody(void *theSprocAsVoid) { struct oneSproc *mySproc = (struct oneSproc *)theSprocAsVoid; /* here one could establish signal handlers, etc. */ for(;;) { sprocSleep(mySproc); /* wait for work to do */ mySproc->calledFunc(mySproc->callArg); /* do the work */ if (mySproc->sprocDone) /* if a completion sema is given, */ usvsema(mySproc->sprocDone); /* ..post it */ } } /* || Acquire a oneSproc structure from the ready list, waiting if necessary. || Called by the master process as part of dispatching a sproc. */ struct oneSproc *getSproc() { struct oneSproc *theSproc; uspsema(readySprocs); /* wait until at least 1 sproc is free */ ussetlock(sprocListLock); /* acquire exclusive rights to sprocList */ theSproc = sprocList; /* get address of first free oneSproc */ sprocList = theSproc->next; /* make next in list, the head of list */ usunsetlock(sprocListLock); /* release sprocList */ return theSproc; } /* || Start a function going asynchronously. Called by master process. */ void execFunc(workFunc toCall, void *callWith, usema_t *done) { struct oneSproc *theSproc = getSproc(); theSproc->calledFunc = toCall; /* set address of func to exec */ theSproc->callArg = callWith; /* set argument to pass */ theSproc->sprocDone = done; /* set sema to post on completion */ usvsema(theSproc->sprocWait); /* wake up sleeping process */ }
When schedctl() is called with the SCHEDMODE argument, it sets one of three scheduling rules for the share group whose member issues the call:
SGS_FREE | The normal situation, in which each process is scheduled individually. |
SGS_SINGLE | All but the master process of the share group are blocked. This permits the master process to perform initialization or error recovery without contention from other members of the group. |
SGS_GANG | All processes of the group run concurrently, provided there are sufficient CPUs available. |
Under gang scheduling, IRIX tries to run all processes of a share group concurrently. When this is possible (in other words, when there are enough available CPUs in the multiprocessor), gang scheduling can greatly reduce lock conflicts between processes.
Without gang scheduling, one member of the share group can acquire a lock and then be suspended. Another member, attempting to acquire the lock, is also suspended until the first process is dispatched again and releases the lock.
With gang scheduling, when a second member attempts to acquire the lock, the first process is almost certainly executing at the same time, and releases the lock while the second member is still spinning.
The sysmp() kernel function provides information about a multiprocessor (detailed in the sysmp(2) reference page). Some of the queries useful to a parallel program include MP_NPROCS, return number of CPUs in the system, and MP_NAPROCS, return the number of CPUs available for normal process scheduling.